package com.magenta.reflection.util;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * Reflection utilities.<br>
 * Contains static cache of fields for reflection.
 */
public final class Reflector {

	//TODO: use weak references
	private static Map<FieldId, Field> cache = new HashMap<Reflector.FieldId, Field>();
	private static Set<Class<?>> cachedClasses = new HashSet<Class<?>>();

	static class FieldId {
		private final Class<?> objectType;
		private final String fieldName;
		FieldId( Class<?> objectType, String fieldName) {
			this.objectType = objectType;
			this.fieldName = fieldName;
		}
		@Override
		public int hashCode() {
			int fieldNameHash = fieldName.hashCode();
			return objectType.hashCode() ^ (fieldNameHash >>> 7 | fieldNameHash << 32 - 7);
		}
		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			FieldId other = (FieldId) obj;
			if (fieldName == null) {
				if (other.fieldName != null)
					return false;
			} else if (!fieldName.equals(other.fieldName))
				return false;
			if (objectType == null) {
				if (other.objectType != null)
					return false;
			} else if (!objectType.equals(other.objectType))
				return false;
			return true;
		}

	}

	private Reflector() {
	}

	private static Field getField(Object object, String fieldName) throws NoSuchFieldException {
		if ( object == null ) {
			throw new IllegalArgumentException("object is null!");
		}
		Class<?> objectType = object.getClass();
		Field field = cache.get(new FieldId(objectType, fieldName));
		if ( field == null ) { //NOT CACHED?
			if ( cachedClasses.contains(objectType) ) {//FIELD NOT EXISTS
				throw new NoSuchFieldException(fieldName);
			}
			//FILL CACHE FOR OBJECT-TYPE
			Class<?> type = objectType;
			cachedClasses.add(objectType);//ONLY REQUESTED CLASS WILL BE CACHED
			while ( type != null ) {
				for ( Field fieldOfType : type.getDeclaredFields() ) {
					fieldOfType.setAccessible(true);
					cache.put(new FieldId(objectType, fieldOfType.getName()), fieldOfType);//ONLY REQUESTED CLASS WILL BE CACHED
				}
				type = type.getSuperclass();
			}
			//FINAL CHECK OF FILLED CACHE
			field = cache.get(new FieldId(objectType, fieldName));
			if ( field == null ) {
				throw new NoSuchFieldException(fieldName);
			}
		}
		return field;
	}

	/**
	 * Set value for field (public, protected or private visibility scope). 
	 * @param object Instance for apply reflection.
	 * @param fieldName Exist field for this instance (from declared fields for class or his parent).
	 * @param value New value for filed.
	 * @return old value.
	 */
	public static Object setValue(Object object, String fieldName, Object value) {
		try {
			Field field = getField(object, fieldName);
			Object oldValue = field.get(object);
			if ( value != null || !field.getType().isPrimitive() ) {//IF FIELD IS PRIMITIVE, NULL CAN'T BE SET
				field.set(object, value);
			}
			return oldValue;
		} catch (SecurityException e) {
			throw new IllegalStateException(e);
		} catch (NoSuchFieldException e) {
			throw new IllegalStateException(e);
		} catch (IllegalArgumentException e) {
			throw new IllegalStateException(e);
		} catch (IllegalAccessException e) {
			throw new IllegalStateException(e);
		}
	}

	/**
	 * Get value of field (public, protected or private visibility scope). 
	 * @param object Instance for apply reflection.
	 * @param fieldName Exist field for this instance (from declared fields for class or his parent).
	 * @return field value.
	 */
	public static Object getValue(Object object, String fieldName) {
		try {
			Field field = getField(object, fieldName);
			return field.get(object);
		} catch (SecurityException e) {
			throw new IllegalStateException(e);
		} catch (NoSuchFieldException e) {
			throw new IllegalStateException(e);
		} catch (IllegalArgumentException e) {
			throw new IllegalStateException(e);
		} catch (IllegalAccessException e) {
			throw new IllegalStateException(e);
		}
	}

	/**
	 * Get value of field @see {@link #getValue(Object, String)} and convert to a String.<br>
	 * If field value is null, conversion to String not apply.
	 * @param object Instance for apply reflection.
	 * @param fieldName Exist field for this instance (from declared fields for class or his parent).
	 * @return field value converted to a string @see {@link String#valueOf(Object)} or null if field is null.
	 */
	public static String getValueAsString(Object object, String fieldName) {
		Object value;
		return (value = getValue(object, fieldName)) != null ? value.toString() : null;
	}

	/**
	 * Try to inspect object instance and return first found field value.
	 * @param object Instance for apply reflection.
	 * @param fieldName Exist field for this instance (from declared fields for class or his parent).
	 * @return type of field.
	 */
	public static Class<?> getFieldType(Object object, String fieldName) {
		Class<?> objectType = object.getClass();
		Field field = cache.get(new FieldId(objectType, fieldName));
		return field != null ? field.getType() : null;
	}

	/**
	 * Find constructor with specified count of arguments and assignable from arguments map.
	 * @param classOfObject class where constructor declared.
	 * @param argumentsCount count of arguments.
	 * @param arguments map of arguments.
	 * @return null if not found constructor otherwise first apply to arguments map.
	 */
	public static Constructor<?> findConstructor(Class<?> classOfObject,
			int argumentsCount, Map<Integer, Object> arguments) {
		for ( Constructor<?> constructor : classOfObject.getDeclaredConstructors() ) {
			Class<?>[] parameterTypes = constructor.getParameterTypes();
			if ( parameterTypes.length == argumentsCount && isAssignable(arguments, parameterTypes) ) {
				return constructor;
			}
		}
		return null;
	}

	private static boolean isAssignable(Map<Integer, Object> arguments,
			Class<?>[] parameterTypes) {
		int argumentIndex = 0;
		for ( Class<?> parameterType : parameterTypes ) {
			Object argument = arguments.get(argumentIndex++);
			if ( argument == null && parameterType.isPrimitive() ) {
				return false;
			}
			if ( argument != null && !parameterType.isAssignableFrom(argument.getClass()) ) {
				return false;
			}
		}
		return true;
	}

}
